'use strict';

const assert = require('assert');

const fixtures = require('../fixtures');
const sharp = require('../../');

const red = { r: 255, g: 0, b: 0, alpha: 0.5 };
const green = { r: 0, g: 255, b: 0, alpha: 0.5 };
const blue = { r: 0, g: 0, b: 255, alpha: 0.5 };

const redRect = {
  create: {
    width: 80,
    height: 60,
    channels: 4,
    background: red
  }
};

const greenRect = {
  create: {
    width: 40,
    height: 40,
    channels: 4,
    background: green
  }
};

const blueRect = {
  create: {
    width: 60,
    height: 40,
    channels: 4,
    background: blue
  }
};

const blends = [
  'over',
  'xor',
  'saturate',
  'dest-over'
];

// Test
describe('composite', () => {
  it('blend', () => Promise.all(
    blends.map(blend => {
      const filename = `composite.blend.${blend}.png`;
      const actual = fixtures.path(`output.${filename}`);
      const expected = fixtures.expected(filename);
      return sharp(redRect)
        .composite([{
          input: blueRect,
          blend
        }])
        .toFile(actual)
        .then(() => {
          fixtures.assertMaxColourDistance(actual, expected);
        });
    })
  ));

  it('premultiplied true', () => {
    const filename = 'composite.premultiplied.png';
    const below = fixtures.path(`input.below.${filename}`);
    const above = fixtures.path(`input.above.${filename}`);
    const actual = fixtures.path(`output.true.${filename}`);
    const expected = fixtures.expected(`expected.true.${filename}`);
    return sharp(below)
      .composite([{
        input: above,
        blend: 'color-burn',
        top: 0,
        left: 0,
        premultiplied: true
      }])
      .toFile(actual)
      .then(() => {
        fixtures.assertMaxColourDistance(actual, expected);
      });
  });

  it('premultiplied false', () => {
    const filename = 'composite.premultiplied.png';
    const below = fixtures.path(`input.below.${filename}`);
    const above = fixtures.path(`input.above.${filename}`);
    const actual = fixtures.path(`output.false.${filename}`);
    const expected = fixtures.expected(`expected.false.${filename}`);
    return sharp(below)
      .composite([{
        input: above,
        blend: 'color-burn',
        top: 0,
        left: 0,
        premultiplied: false
      }])
      .toFile(actual)
      .then(() => {
        fixtures.assertMaxColourDistance(actual, expected);
      });
  });

  it('premultiplied absent', () => {
    const filename = 'composite.premultiplied.png';
    const below = fixtures.path(`input.below.${filename}`);
    const above = fixtures.path(`input.above.${filename}`);
    const actual = fixtures.path(`output.absent.${filename}`);
    const expected = fixtures.expected(`expected.absent.${filename}`);
    return sharp(below)
      .composite([{
        input: above,
        blend: 'color-burn',
        top: 0,
        left: 0
      }])
      .toFile(actual)
      .then(() => {
        fixtures.assertMaxColourDistance(actual, expected);
      });
  });

  it('multiple', () => {
    const filename = 'composite-multiple.png';
    const actual = fixtures.path(`output.${filename}`);
    const expected = fixtures.expected(filename);
    return sharp(redRect)
      .composite([{
        input: blueRect,
        gravity: 'northeast'
      }, {
        input: greenRect,
        gravity: 'southwest'
      }])
      .toFile(actual)
      .then(() => {
        fixtures.assertMaxColourDistance(actual, expected);
      });
  });

  it('zero offset', done => {
    sharp(fixtures.inputJpg)
      .resize(80)
      .composite([{
        input: fixtures.inputPngWithTransparency16bit,
        top: 0,
        left: 0
      }])
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(3, info.channels);
        fixtures.assertSimilar(fixtures.expected('overlay-offset-0.jpg'), data, done);
      });
  });

  it('offset and gravity', done => {
    sharp(fixtures.inputJpg)
      .resize(80)
      .composite([{
        input: fixtures.inputPngWithTransparency16bit,
        left: 10,
        top: 10,
        gravity: 4
      }])
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(3, info.channels);
        fixtures.assertSimilar(fixtures.expected('overlay-offset-with-gravity.jpg'), data, done);
      });
  });

  it('offset, gravity and tile', done => {
    sharp(fixtures.inputJpg)
      .resize(80)
      .composite([{
        input: fixtures.inputPngWithTransparency16bit,
        left: 10,
        top: 10,
        gravity: 4,
        tile: true
      }])
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(3, info.channels);
        fixtures.assertSimilar(fixtures.expected('overlay-offset-with-gravity-tile.jpg'), data, done);
      });
  });

  it('offset and tile', done => {
    sharp(fixtures.inputJpg)
      .resize(80)
      .composite([{
        input: fixtures.inputPngWithTransparency16bit,
        left: 10,
        top: 10,
        tile: true
      }])
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(3, info.channels);
        fixtures.assertSimilar(fixtures.expected('overlay-offset-with-tile.jpg'), data, done);
      });
  });

  it('cutout via dest-in', done => {
    sharp(fixtures.inputJpg)
      .resize(300, 300)
      .composite([{
        input: Buffer.from('<svg><rect x="0" y="0" width="200" height="200" rx="50" ry="50"/></svg>'),
        density: 96,
        blend: 'dest-in',
        cutout: true
      }])
      .png()
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('png', info.format);
        assert.strictEqual(300, info.width);
        assert.strictEqual(300, info.height);
        assert.strictEqual(4, info.channels);
        fixtures.assertSimilar(fixtures.expected('composite-cutout.png'), data, done);
      });
  });

  describe('numeric gravity', () => {
    Object.keys(sharp.gravity).forEach(gravity => {
      it(gravity, done => {
        sharp(fixtures.inputJpg)
          .resize(80)
          .composite([{
            input: fixtures.inputPngWithTransparency16bit,
            gravity: gravity
          }])
          .toBuffer((err, data, info) => {
            if (err) throw err;
            assert.strictEqual('jpeg', info.format);
            assert.strictEqual(80, info.width);
            assert.strictEqual(65, info.height);
            assert.strictEqual(3, info.channels);
            fixtures.assertSimilar(fixtures.expected(`overlay-gravity-${gravity}.jpg`), data, done);
          });
      });
    });
  });

  describe('string gravity', () => {
    Object.keys(sharp.gravity).forEach(gravity => {
      it(gravity, done => {
        const expected = fixtures.expected('overlay-gravity-' + gravity + '.jpg');
        sharp(fixtures.inputJpg)
          .resize(80)
          .composite([{
            input: fixtures.inputPngWithTransparency16bit,
            gravity: sharp.gravity[gravity]
          }])
          .toBuffer((err, data, info) => {
            if (err) throw err;
            assert.strictEqual('jpeg', info.format);
            assert.strictEqual(80, info.width);
            assert.strictEqual(65, info.height);
            assert.strictEqual(3, info.channels);
            fixtures.assertSimilar(expected, data, done);
          });
      });
    });
  });

  describe('tile and gravity', () => {
    Object.keys(sharp.gravity).forEach(gravity => {
      it(gravity, done => {
        const expected = fixtures.expected('overlay-tile-gravity-' + gravity + '.jpg');
        sharp(fixtures.inputJpg)
          .resize(80)
          .composite([{
            input: fixtures.inputPngWithTransparency16bit,
            tile: true,
            gravity: gravity
          }])
          .toBuffer((err, data, info) => {
            if (err) throw err;
            assert.strictEqual('jpeg', info.format);
            assert.strictEqual(80, info.width);
            assert.strictEqual(65, info.height);
            assert.strictEqual(3, info.channels);
            fixtures.assertSimilar(expected, data, done);
          });
      });
    });
  });

  describe('validation', () => {
    it('missing images', () => {
      assert.throws(() => {
        sharp().composite();
      }, /Expected array for images to composite but received undefined of type undefined/);
    });

    it('invalid images', () => {
      assert.throws(() => {
        sharp().composite(['invalid']);
      }, /Expected object for image to composite but received invalid of type string/);
    });

    it('missing input', () => {
      assert.throws(() => {
        sharp().composite([{}]);
      }, /Unsupported input/);
    });

    it('invalid blend', () => {
      assert.throws(() => {
        sharp().composite([{ input: 'test', blend: 'invalid' }]);
      }, /Expected valid blend name for blend but received invalid of type string/);
    });

    it('invalid tile', () => {
      assert.throws(() => {
        sharp().composite([{ input: 'test', tile: 'invalid' }]);
      }, /Expected boolean for tile but received invalid of type string/);
    });

    it('invalid premultiplied', () => {
      assert.throws(() => {
        sharp().composite([{ input: 'test', premultiplied: 'invalid' }]);
      }, /Expected boolean for premultiplied but received invalid of type string/);
    });

    it('invalid left', () => {
      assert.throws(() => {
        sharp().composite([{ input: 'test', left: 0.5 }]);
      }, /Expected positive integer for left but received 0.5 of type number/);
    });

    it('invalid top', () => {
      assert.throws(() => {
        sharp().composite([{ input: 'test', top: -1 }]);
      }, /Expected positive integer for top but received -1 of type number/);
    });

    it('left but no top', () => {
      assert.throws(() => {
        sharp().composite([{ input: 'test', left: 1 }]);
      }, /Expected both left and top to be set/);
    });

    it('top but no left', () => {
      assert.throws(() => {
        sharp().composite([{ input: 'test', top: 1 }]);
      }, /Expected both left and top to be set/);
    });

    it('invalid gravity', () => {
      assert.throws(() => {
        sharp().composite([{ input: 'test', gravity: 'invalid' }]);
      }, /Expected valid gravity for gravity but received invalid of type string/);
    });
  });
});
